﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using HeritageAcademy.Models.DataModels;
using HeritageAcademy.Shared;
using HeritageAcademy.Shared.Properties;
using ICSharpCode.SharpZipLib.Zip;

namespace HeritageAcademy.Models.Loaders
{
    public static class CourseLoader
    {
        #region Constants
        
        private static readonly string COURSE_FILE_FILTER = Resources.Course_PackageExtension;
        // TODO: the creation of XmlSerializer is costly therefore it is recommended to cache it if it used often
        private static readonly XmlSerializer s_serializer = new XmlSerializer(typeof(Course));
        
        #endregion

        #region Public Methods
        
        /// <summary>
        /// Load books from the file system
        /// </summary>
        /// <param name="sourceFolder">Source folder to load books from</param>
        /// <returns>List of loaded books</returns>
        public static IEnumerable<Course> LoadCourses(string sourceFolder)
        {
            // TODO: Async
            var sourcePath = IOHelper.GetFullFilePath(sourceFolder);
            var files = Directory.GetFiles(sourcePath, COURSE_FILE_FILTER, SearchOption.AllDirectories);
            return files.Select(GetFromZip).ToList();
        }

        public static Course GetFromDisk(string ebookFilePath)
        {
            Course course;
            using (var srm = File.OpenRead(ebookFilePath))
                course = GetFromStream(srm);
            var bookFolder = Path.GetDirectoryName(ebookFilePath);
            course.Folder = bookFolder;
            return course;
        }

        private static Course GetFromStream(Stream ebookFileSream)
        {
            Course course;
            using (TextReader reader = new StreamReader(ebookFileSream))
                course = (Course) s_serializer.Deserialize(reader);
            SetLocalChapters(course);
            return course;
        }

        /// <summary>
        /// Get the chapter stream from the course zip file
        /// </summary>
        /// <param name="course">Course</param>
        /// <param name="fileName">File name</param>
        /// <returns>Stream</returns>
        private static Stream GetItemFromZip(Course course, string fileName)
        {
            var chapterName = Path.GetFileName(fileName);
            Predicate<string> criteria = f => f.EndsWith(chapterName, StringComparison.InvariantCultureIgnoreCase);
            var srm = GetItemFromZip(course, criteria);
            return srm;
        }

        /// <summary>
        /// Extract course from Zip file and keep it in memory
        /// </summary>
        /// <param name="course"></param>
        /// <param name="criteria">Filter criteria</param>
        /// <returns>
        /// course must exists
        /// </returns>
        private static Stream GetItemFromZip(Course course, Predicate<string> criteria)
        {
            return GetItemFromZip(course.ZipPath, criteria);
        }

        /// <summary>
        /// Extract course from Zip file and keep it in memory
        /// </summary>
        /// <param name="path"></param>
        /// <param name="criteria"></param>
        /// <returns>
        /// course must exists
        /// </returns>
        private static Stream GetItemFromZip(string path, Predicate<string> criteria)
        {
            if (criteria == null) throw new ArgumentNullException("criteria");
            var data = LoadZippedContentSingle(path, criteria);
            return data == null ? new MemoryStream() : data.ToStream();
        }

        /// <summary>
        /// Load course from .course file and keep it in memory
        /// </summary>
        /// <param name="path">Path to .course file</param>
        /// <returns>Loaded course</returns>
        /// <remarks>Course must exists</remarks>
        public static Course GetFromZip(string path)
        {
            var ext = Resources.Course_MetaDataExtension;
            Predicate<string> criteria = s => s.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase);
            var courseStream = GetItemFromZip(path, criteria);
            var course = GetFromStream(courseStream);
            var folder = Path.GetFileNameWithoutExtension(path);
            course.Folder = Path.Combine(IOHelper.DefaultCoursesFolders.FirstOrDefault(), folder);
            course.ZipPath = path;
            return course;
        }

        /// <summary>
        /// Get the chapter srteam form the course zip file
        /// </summary>
        /// <param name="course"></param>
        /// <param name="chapterIndex"></param>
        /// <returns></returns>
        public static Stream GetChapterStreamFromZip(Course course, int chapterIndex)
        {
            var fileName = course.GetChapterFileName(chapterIndex);
            return GetItemFromZip(course, fileName);
        }

        /// <summary>
        /// Extracts course images into a temporary image folder.
        /// </summary>
        /// <param name="course">Course to extract images</param>
        public static void ExtractImages(Course course)
        {
            var separator = Convert.ToChar(Resources.Files_TypeSeparator);
            var allowedImageTypes = Resources.Files_AllowedImageExtensions.Split(separator);

            Predicate<string> criteria = filename => allowedImageTypes.Contains(Path.GetExtension(filename));

            var tempImagesFolder = IOHelper.CourseTemporaryFolder;

            using (var zipedFolder = File.OpenRead(course.ZipPath))
            using (var inputStream = new ZipInputStream(zipedFolder))
            {
                ZipEntry theEntry;
                while ((theEntry = inputStream.GetNextEntry()) != null)
                {
                    if (!theEntry.IsFile) continue;
                    if (!criteria(theEntry.Name)) continue;

                    var buffer = new byte[theEntry.Size];
                    var size = inputStream.Read(buffer, 0, buffer.Length);
                    if (size == 0) continue;
                    var imageFileName = Path.Combine(tempImagesFolder, theEntry.Name);

                    if (!Directory.Exists(Path.GetDirectoryName(imageFileName)))
                        Directory.CreateDirectory(Path.GetDirectoryName(imageFileName));

                    File.WriteAllBytes(imageFileName, buffer);
                }
                inputStream.Close();
            }
        }

        /// <summary>
        /// Extract course test files to a temporary folder.
        /// </summary>
        /// <param name="course">Course to extract tests</param>
        public static void ExtractTests(Course course)
        {
            var allowedTestsTypes = Resources.Test_FileExtension;

            Predicate<string> criteria = filename => allowedTestsTypes.Contains(Path.GetExtension(filename));

            var tempTestsFolder = IOHelper.CourseTemporaryFolder;

            using (var zipedFolder = File.OpenRead(course.ZipPath))
            using (var inputStream = new ZipInputStream(zipedFolder))
            {
                ZipEntry theEntry;
                while ((theEntry = inputStream.GetNextEntry()) != null)
                {
                    if (!theEntry.IsFile) continue;
                    if (!criteria(theEntry.Name)) continue;

                    var buffer = new byte[theEntry.Size];
                    var size = inputStream.Read(buffer, 0, buffer.Length);
                    if (size == 0) continue;
                    var testsFileName = Path.Combine(tempTestsFolder, theEntry.Name);

                    if (!Directory.Exists(Path.GetDirectoryName(testsFileName)))
                        Directory.CreateDirectory(Path.GetDirectoryName(testsFileName));

                    File.WriteAllBytes(testsFileName, buffer);
                }
                inputStream.Close();
            }
        }

        /// <summary>
        /// Code from http://www.codeproject.com/KB/cs/Zip_UnZip.aspx.
        /// </summary>
        /// <param name="SrcFile">Source file</param>
        /// <param name="DstFile">Destination file</param>
        /// <param name="BufferSize">Buffer size (4096 is good)</param>
        public static void UnZip(string SrcFile, string DstFile, int BufferSize)
        {
            var fileStreamIn = new FileStream(SrcFile, FileMode.Open, FileAccess.Read);
            var zipInStream = new ZipInputStream(fileStreamIn);

            ZipEntry entry;
            while ((entry = zipInStream.GetNextEntry()) != null)
            {
                var fileName = Path.GetFileName(entry.Name);
                var folderName = Path.GetDirectoryName(entry.Name);
                Directory.CreateDirectory(DstFile + @"\" + folderName);
                if (fileName == string.Empty) continue;
                var fileStreamOut = new FileStream(DstFile + @"\" + entry.Name, FileMode.Create, FileAccess.Write);

                int size;
                var buffer = new byte[BufferSize];
                do
                {
                    size = zipInStream.Read(buffer, 0, buffer.Length);
                    fileStreamOut.Write(buffer, 0, size);
                } while (size > 0);

                fileStreamOut.Close();
            }

            zipInStream.Close();
            fileStreamIn.Close();
        }

        public static void UnzipTests(string title)
        {
            var srcFileName = String.Format("{0}.tests", title).Normalize();
            var srcFile = Path.Combine(IOHelper.DefaultCoursesFolders.FirstOrDefault(), srcFileName);
            var dstFile = Path.Combine(IOHelper.CourseTemporaryFolder, title);
            UnZip(srcFile, dstFile, 4096);
        }
        
        #endregion

        #region Private Methods
        
        /// <summary>
        /// Extract single file content from zip file
        /// </summary>
        /// <param name="path">the zip file path of the course</param>
        /// <param name="criteria">Criteria for relavant extact entities</param>
        /// <returns></returns>
        private static CourseRawData LoadZippedContentSingle(string path, Predicate<string> criteria)
        {
            CourseRawData data = null;
            using (var zipedFolder = File.OpenRead(path))
            using (var inputStream = new ZipInputStream(zipedFolder))
            {
                ZipEntry theEntry;
                while ((theEntry = inputStream.GetNextEntry()) != null)
                {
                    if (!theEntry.IsFile) continue;
                    if (!criteria(theEntry.Name)) continue;

                    var buffer = new byte[theEntry.Size];
                    var size = inputStream.Read(buffer, 0, buffer.Length);
                    if (size != 0) data = new CourseRawData(buffer);
                    break;
                }
                inputStream.Close();
            }

            return data;
        }

        private static void SetLocalChapters(Course pBook)
        {
            foreach (var c in pBook.Chapters)
                AddChapterToLocalChapters(pBook, c);
        }

        private static void AddChapterToLocalChapters(Course pCourse, Chapter pChapter)
        {
            pCourse.FlatChapters.Add(pChapter);
            foreach (var c in pChapter.Chapters)
                AddChapterToLocalChapters(pCourse, c);
        }
        
        #endregion
    }
}